﻿#Requires AutoHotkey v2.0

#Include ..\_lib\Extend.ahk
#Include ..\_lib\Tip.ahk
#Include ..\_lib\Cursor.ahk
#Include ..\_lib\Theme.ahk
#Include ..\_lib\Animation.ahk

#Include ..\_lib\MeowConf.ahk
#Include ..\_lib\MeowConfEx.ahk

#Include ..\core\util.ahk
#Include ..\core\handleMgr.ahk
#Include ..\core\historyMgr.ahk
#Include ..\core\completion.ahk

nd := ''
#Include ..\_lib\Debug.ahk

#Include constants.ahk
#Include sub\output.ahk
#Include sub\extra.ahk
#Include sub\cmds.ahk

#HotIf MeowTool.ToolIsActive()
up:: MeowTool.SetContent(';' History.Get())
down:: MeowTool.SetContent(';' History.Get(false))
^Right:: MeowTool.ins._DisplayExtraArea()
^Left:: MeowTool.ins._HideExtraArea()
Tab:: MeowTool.ins.HandleAutoComplete()
#HotIf

class MeowTool extends Gui {

  static ins := MeowTool()
    , exitFlag := 'x', reloadFlag := 'r', extraFlag := 'e', asyncFlag := 's'
    , maxLen := 30, maxH := maxMsg * 21 + paddingBottom + minH

  class Green extends Theme.Themes {
    __New() {
      super.__New()
      this.window_Bgc := '#F4FAFE', this.default_Fc := '#003711'
      this.edit_Fc := '#264b0c', this.edit_Bgc := '#f4fcff'
    }
  }

  __New() {
    super.__New('+AlwaysOnTop +ToolWindow -Caption +Border')
    this.SetFont('s16', 'Consolas')
    this.AddButton('w0 h0 xs Default').OnEvent('click', (*) => this.Handle())
    this.AddPicture('x0 y0 w672 h55', './ui/bg/blur.png')
    this.AddPicture('x650 y55 h800 w-1', './ui/bg/cherry.png') ; size: 20*?
    this.AddPicture('x0 y55 h800 w-1', './ui/bg/lemon.png') ; size: 20*?
    this.edit := this.AddEdit('section xm ym w300 h30 -Multi') ; size: 30*?
    this.sbtn := this.AddText('yp w25 ym+3 x+2 BackgroundTrans', '·')
    this.sbtn.OnEvent('ContextMenu', (*) => this._ToggleExtraArea())
    this.et := this.AddText('Center x+0 w300 h30 BackgroundTrans', '{x-x}')
    this.h := minH, this.sh := 0, this.loading := this.extraVisible := this.expanded := false
    this.SetFont('s12'), Theme.Custom(this, MeowTool.Green())
    this.edit.OnEvent('change', (g, *) => this._Suggestion(g.value))
    this.st := this.AddText('0x80 x20 y' this.h + 2 ' w300 Hidden')
    ; this.st.OnEvent('ContextMenu', (v, *) => this.OnCopy(v))
    this.OnEvent('DropFiles', (g, o, f, *) => this.OnDropFiles(f[1]))
    this.builtInCMD := Map('cls', this.ClearHistory, 'expand', this.Expand, 'fit', this.Fit)

    this.out := OutputGui(this.Hwnd)
    this.out.Show(20, 57, 'NA')

  }

  static Hide() => MeowTool.ins.Hide()
  static Resume() => MeowTool.ins.Show()
  static ToolIsActive() => WinActive(MeowTool.ins)
  static GetCmdList() {
    static sorted := groupAndSortStrings(Mgr.h.Keys.Join(','), ',')
      .concat([{ letter: '#', words: MeowTool.ins.builtInCMD.keys }, { letter: '%', words: alias.data.keys }])
    return sorted
  }
  static SetContent(content) {
    if !content or content == MeowTool.ins.edit.Text
      return
    MeowTool.ins.edit.Text := content
    MeowTool.ins._moveCursorToEnd()
    MeowTool.ins._Suggestion(content)
  }

  static Show() {
    Animation.RollDown(MeowTool.ins, Noop, (*) => MeowTool.ins.Move(A_ScreenWidth // 2 - minW // 2, A_ScreenHeight // 4, minW, minH))
    FrameShadow(MeowTool.ins.Hwnd), MeowTool.ins.FocusInput()

    FrameShadow(HGui) {
      DllCall("dwmapi\DwmIsCompositionEnabled", "IntP", &_ISENABLED := 0) ; Get if DWM Manager is Enabled
      if !_ISENABLED ; if DWM is not enabled, Make Basic Shadow
        DllCall("SetClassLong", "UInt", HGui, "Int", -26, "Int", DllCall("GetClassLong", "UInt", HGui, "Int", -26) | 0x20000)
      else {
        _MARGINS := Buffer(16)
        NumPut("UInt", 1, _MARGINS, 0)
        NumPut("UInt", 1, _MARGINS, 4)
        NumPut("UInt", 1, _MARGINS, 8)
        NumPut("UInt", 1, _MARGINS, 12)
        DllCall("dwmapi\DwmSetWindowAttribute", "Ptr", HGui, "UInt", 2, "Int*", 2, "UInt", 4)
        DllCall("dwmapi\DwmExtendFrameIntoClientArea", "Ptr", HGui, "Ptr", _MARGINS.Ptr)
      }
    }

  }

  _DisplayExtraArea() => (this.extraVisible := true, this._ChangeW(maxW), this.sbtn.Text := '<<')
  _HideExtraArea() => (this.extraVisible := false, this._ChangeW(minW), this.sbtn.Text := '·')
  _ToggleExtraArea() => this.extraVisible ? this._HideExtraArea() : this._DisplayExtraArea()
  _Blink() {
    this._Loading(100)
    SetTimer((*) => this._LoadingDone(), -500)
  }
  _ChangeW(_w) {
    this.GetPos(, , &w), i := _w > w ? 1 : -1
    loop Abs(_w - w)
      this.Move(, , w + A_Index * i)
  }
  _ChangeH(_h, instant := false) {
    if this.expanded
      return
    this.GetPos(, , , &h)
    if instant {
      this.Move(, , , _h)
    } else {
      i := _h > h ? 1 : -1
      loop Abs(_h - h)
        this.Move(, , , h + A_Index * i)
    }
    this.h := _h
  }
  _Loading(frequency := 500) {
    if this.loading
      return
    this.loading := true
    s1 := () => this.extraVisible ? '--' : '|', s2 := () => this.extraVisible ? '<<' : '·'
    this.loadingFn := (*) => this.sbtn.Text := this.sbtn.Text = s2() ? s1() : s2()
    SetTimer this.loadingFn, frequency
  }
  _LoadingDone() => (SetTimer(this.loadingFn, 0), this.loading := false, this.sbtn.Text := this.extraVisible ? '<<' : '·')

  _Exit() => (Sleep(1000), Animation.RollUp(MeowTool.ins), ExitApp())
  _Reload() => (Sleep(1000), Reload())
  _Clear() => this.edit.Text := ''

  _Suggestion(cmd) {
    Critical
    if this.loading
      return
    if cmd = '' {
      Suggestion.clear()
      return this._CloseSuggestion()
    }
    if !this.extraVisible
      this.out.Hide()
    this.st.Visible := true, _setStBG(stColors.normal)
    allCmdGui := CmdsArea.getIns(this.Hwnd), allCmdGui.Hide()
    if cmd = ';' { ; 显示命令列表
      this.st.Visible := false, lastHist := History.hist.Length ? History.hist.at(-1) : ''
      allCmdGui.Show(this.extraVisible ? 350 : 20)
      Suggestion.setCur(lastHist.Length + 1, '', lastHist) ; 此时补全为上一条历史命令
      _adjustHeight(allCmdGui.getH(), true)
      return
    }
    if cmd[1] = ';'
      cmd := cmd.substring(2)
    if !(cmd := LTrim(cmd))
      return this._CloseSuggestion()
    ks := [], _k := (i := InStr(cmd, A_Space)) ? SubStr(cmd, 1, i - 1) : cmd
    if i { ; 命令已确定
      if Mgr.h.Has(_k) and this._getCursorCol() > i {
        cmd := cmd.substring(1, this._getCursorCol())
        return _showSgAndFit(Suggestion.getList(cmd, _setStBG))
      } else if alias.Has(_k) {
        return _showSgAndFit(alias.Get(_k) '`n--`nALIAS ' _k)
      } else if this.builtInCMD.Has(_k)
        return _showSgAndFit(_k '-builtin' '`n--`nBUILTIN')
    }
    ; #region 过滤命令
    sorted := MeowTool.GetCmdList(), letter := _k[1].toUpperCase()
    if idx := sorted.findIndex(v => v.letter == letter)
      sorted[idx].words.foreach(v => beginWith(v, _k) && ks.Push(v))
    sorted[-1].words.foreach(v => beginWith(v, _k) && ks.Push(v))
    sorted[-2].words.foreach(v => beginWith(v, _k) && ks.Push(v))
    ; #endregion

    if ks.Length = 1 { ; 最佳匹配
      _setStBG(stColors.best)
      r := alias.Get(ks[1], Mgr.h.Has(ks[1]) ? Suggestion.getList(ks[1], _setStBG) : ks[1] '-builtin')
      Suggestion.setCur(_k.Length, _k, ks[1])
      Suggestion.setSec(_k.Length, ks[1], ks[1] A_Space)
    } else if !ks.Length { ; 无匹配
      r := 'nil'
      _setStBG(stColors.null)
      Suggestion.clear()
    } else { ; 所有可能的命令
      r := ks.Join(','), _p := longestCommonPrefix(ks)
      if ks[1] = _k {
        r .= '`n-`n' alias.Get(ks[1], Mgr.h.Has(ks[1]) ? Suggestion.getList(ks[1], _setStBG) : ks[1] '-builtin')
        _setStBG(stColors.normal)
      }
      Suggestion.setCur(_k.Length, _k, _p)
      Suggestion.setSec(_k.Length, _k, ks[1])
    }

    return _showSgAndFit(r)

    _showSgAndFit(str) {
      this.st.Text := slice(str, MeowTool.maxLen + 3, '`n'), this.st.raw := str
      this.st.Move(this.extraVisible ? 350 : 20, , , (this.st.Text.Count('`n') + 1) * 20), this.st.GetPos(, , , &h)
      if h = this.sh
        return
      Sleep(10), _adjustHeight(h)
    }

    _adjustHeight(h, instant := false) {
      this.sh := h, c := (this.sh = 0 && this.out.h - this.sh <= 0)
      h := this.extraVisible ? Max(h, this.out.h) : h ; 在展开时，取较大值
      this._ChangeH(minH + h + paddingBottom, c | instant)
    }

    _setStBG(rgb) => this.st.Opt('Background' rgb)
  }

  _CloseSuggestion(fit := true) {
    if !this.sh
      return
    Suggestion.clear()
    CmdsArea.getIns(this.Hwnd).Hide()
    this.st.Text := '', this.st.Visible := false
    this.out.Resume()
    if !fit
      return
    (this.out.h) ? this._ChangeH(minH + this.out.h + paddingBottom) : this._ChangeH(minH)
    this.sh := 0
  }

  Handle() {
    static cnt := 0
    if GetKeyState('LCtrl', 'P')
      this.HandleAutoComplete()
    cmd := Trim(this.edit.Text)
    if cmd.beginWith(';')
      doHist := false, cmd := cmd.substring(2)
    if !cmd {
      this.AddHistory(cnt++, , , , { align: 'right' })
      this._CloseSuggestion(), this._Clear()
      return
    }
    if this.builtInCMD.Has(cmd)
      return this.builtInCMD.Get(cmd)(this)
    this.AddHistory(cmd, true), this._Loading(), this._Clear()
    this._CloseSuggestion()
    if (r := Mgr.Check(cmd)).valid {
      echo := Mgr.Call(r.handler, r.parsed), this._LoadingDone()
      History.Add(cmd, (!IsSet(doHist) and echo.flag))
      switch echo.extra {
        case MeowTool.reloadFlag: return _handleReload()
        case MeowTool.exitFlag: return _handleExit(echo.r)
        case MeowTool.extraFlag: _handleExtra(echo.r, echo.flag)
        case MeowTool.asyncFlag: _handleAsync(echo.r)
        default: this.AddHistory(echo.r, , echo.flag)
      }
    } else {
      this.AddHistory(r.msg '---' TruncatedString(cmd, 10), , false), this._LoadingDone()
      History.Add(cmd, false)
    }

    _handleReload() => (this.AddHistory('reload.'), this._Reload())
    _handleExit(_r) => (this.AddHistory(_r), this._Exit())

    _handleExtra(_r, _flag) {
      this.AddHistory('see extra.', , _flag)
      if this.HasProp('extra')
        this.extra.Destroy()
      this.Expand()
      this.et.Text := _r.t
      this.extra := ExtraArea(this.Hwnd, _r.g)
      this.extra.Show(340, 57, 'NA')
      this.FocusInput()
    }

    _handleAsync(_r) {
      _r.done := (_, res, flag) => this.AddHistory(res, , flag, true)
      _r.succ := (_, res) => this.AddHistory(res, , , true)
      _r.fail := (_, res) => this.AddHistory(res, , false, true)
      SetTimer (*) => _r(), -1
    }
    _checkoutLen(text) => text.Length // MeowTool.maxLen >= maxMsg
  }

  OnDropFiles(f) {
    this.edit.Text .= ' ' _escape(f)

    _escape(t) {
      t := StrReplace(t, '\', '\\')
      return InStr(t, A_Space) ? SurroundWith(t, "'") : t
    }
  }

  HandleAutoComplete() {
    sg := Suggestion.get(this.edit.Text)
    if !sg {
      this._Blink()
      return
    }
    this.edit.Text := sg.t
    this._setCursorCol(sg.c, sg.c)
    this._Suggestion(this.edit.Text)
  }

  _setCursorCol(left, right) => SendMessage(0x00B1, left, right, this.edit)
  _getCursorCol() => EditGetCurrentCol(this.edit)
  _moveCursorToHead() => this._setCursorCol(0, 0)
  _moveCursorToEnd() => this._setCursorCol(this.edit.Text.Length, this.edit.Text.Length)

  FocusInput() => ControlFocus(this.edit)

  ClearHistory() {
    this.expanded := false
    this.out.Destroy(), (this.out := OutputGui(this.Hwnd)).Show(20, 55, 'NA')
    this.FocusInput()
    this._Clear(), this._CloseSuggestion(), this._HideExtraArea()
  }

  Expand() {
    if this.expanded
      return
    this._Clear(), this._CloseSuggestion(false), this._DisplayExtraArea()
    this._ChangeH(MeowTool.maxH)
    this.expanded := true
    this.AddHistory('Expanded!')
  }

  Fit() {
    this.expanded := false
    this._Clear(), this._CloseSuggestion()
  }

  AddHistory(text, isInput := false, succ := true, async := false, options := {}) {
    this.out.Add(text, isInput, succ, async, options)
    this._ChangeH(minH + this.out.h + paddingBottom)
    this.h -= paddingBottom
  }

}